/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2021-2022. All rights reserved.
 *
 */

import { cloneDeep, difference, intersection, isEqual, union } from '@baize/wdk';
import { FileSet, replaceBundleName } from '@ohos/hvigor';
import * as fs from 'fs-extra';

import { CommonConst } from '../../const/common-const.js';
import { ApiVersion } from '../../const/sdk-const.js';
import {
  HasPlaceholderOptions,
  IsMapFields,
  IsNeedMergeRule,
  KeepAbilityMergeList,
  KeepHapOptions,
  KeepModuleAttr,
  KeepOneOfThem,
  NeedDisposePlaceholder,
  UniqueKeyEqualsName,
} from '../../enum/merge-type-rule.js';
import { LegacyModuleTargetRes } from '../../model/res/res-model.js';
import { LegacyTargetSourceSetModel } from '../../model/source-set/source-set-model.js';
import { ProjectBuildProfile } from '../../options/build/project-build-profile.js';
import { ConfigJson } from '../../options/configure/config-json-options.js';
import { Named, Option } from '../../options/options.js';
import { OhosLogger } from '../../utils/log/ohos-logger.js';
import { AbstractMergeProfile } from '../abstract-merge-profile.js';
import { TaskNames } from '../common/task-names.js';
import { TargetTaskService } from '../service/target-task-service.js';

import { LegacyPreBuild } from './legacy-pre-build.js';
import ConfigOptObj = ConfigJson.ConfigOptObj;
import DeviceConfigOptionObj = ConfigJson.DeviceConfigOptionObj;
import MergeRuleObj = ConfigJson.MergeRuleObj;
import LegacyFATask = TaskNames.LegacyFATask;
import { DeviceTypeConst } from '../../const/device-type-const';
import { HarTargetUtil } from '../../utils/har-target-util.js';
import { getJson5Obj } from '../../utils/json-util.js';
import { ShellUtils } from '../../utils/shell-utils';

/**
 * 合并config.json
 *
 * @since 2022/1/19
 */
export class LegacyMergeProfile extends AbstractMergeProfile {
  private _log: OhosLogger = OhosLogger.getLogger(LegacyMergeProfile.name);

  private _moduleTargetRes: LegacyModuleTargetRes;
  private _appBundleName: string | undefined;

  private readonly _harLibs: string[];
  private readonly _mergedModuleJson: string;

  constructor(taskService: TargetTaskService) {
    super(taskService, LegacyFATask.MERGE_PROFILE);

    this._harLibs = this.mergeDependsLibs(CommonConst.CONFIG_JSON);

    const targetSourceSetModel: LegacyTargetSourceSetModel = this.service
      .getModuleModel()!
      .getSourceSetByTargetName(this._targetName) as LegacyTargetSourceSetModel;
    this._moduleTargetRes = targetSourceSetModel.getLegacyModuleTargetRes();
    this._mergedModuleJson = this._pathInfo.getIntermediatesMergeLegacyProfile();
  }

  initTaskDepends() {
    this.declareDepends(LegacyPreBuild.name);
  }

  protected initHarModuleDepends() {
    const depHarTargets = HarTargetUtil.calDepHarTargets(this.targetService);
    depHarTargets.forEach((targetName, harName) => this.declareDepends(`${targetName}@${LegacyFATask.MERGE_PROFILE.name}`, harName));
  }

  mergeDslConfig(configOpt: ConfigOptObj): void {
    if (this._bundleName) {
      configOpt.app.bundleName = this._bundleName;
      this._appBundleName = this._bundleName;
      this._log.debug(replaceBundleName(`Change app bundleName with '${this._bundleName}'.`, this._bundleName));
    }

    if (this._vendor) {
      configOpt.app.vendor = this._vendor;
      this._log.debug(`Change app vendor with '${this._vendor}'.`);
    }

    const apiMeta: ProjectBuildProfile.ProductApiMeta = this.targetData.getApiMeta();
    const toOriginalAPI = !![
      apiMeta.compileSdkVersion.version,
      apiMeta.compatibleSdkVersion.version,
      apiMeta.targetSdkVersion?.version ?? apiMeta.compileSdkVersion.version,
    ].find((version) => version <= ApiVersion.API_VERSION_9);

    const targetVersion = apiMeta.targetSdkVersion ?? apiMeta.compileSdkVersion;
    const target = this.targetData.isHarmonyOS() ? this.apiTransform(targetVersion) : targetVersion.version;
    const min = this.targetData.isHarmonyOS() ? this.apiTransform(apiMeta.compatibleSdkVersion) : apiMeta.compatibleSdkVersion.version;
    const compileSdkType: string = this.targetData.isHarmonyOS() ? CommonConst.HARMONY_OS : CommonConst.OPEN_HARMONY;
    const compileSdkVersion = this.sdkInfo.getToolchainsComponentVersion();
    configOpt.app.apiVersion = {
      compileSdkVersion: compileSdkVersion,
      compileSdkType: compileSdkType,
      target: toOriginalAPI ? targetVersion.version : target,
      compatible: toOriginalAPI ? apiMeta.compatibleSdkVersion.version : min,
      releaseType: this.sdkInfo.getReleaseType(),
    };
    this._log.debug(`Change app target API version with '${configOpt.app.apiVersion.target}'`);
    this._log.debug(`Change app minimum API version with '${configOpt.app.apiVersion.compatible}'`);
    this._log.debug(`Change app releaseType with '${this.sdkInfo.getReleaseType()}'`);
  }

  protected doTaskAction(): void {
    const configOpt: ConfigOptObj = cloneDeep(this._moduleTargetRes.getConfigJsonOpt());
    this.mergeAsanAndTsan(configOpt);
    this.mergeAppEnvironmentConfig(configOpt);
    this._appBundleName = configOpt.app.bundleName;

    const harConfigOpts: ConfigOptObj[] = this._harLibs.map((harConfigJson) => {
      if (fs.pathExistsSync(harConfigJson)) {
        return getJson5Obj(harConfigJson);
      } else {
        this._log.warn(`${harConfigJson} not found, The library will not be merged.
         Check the configuration of this module.`);
        return null;
      }
    });

    // 合并hapConfig和所有harConfig
    const mergedConfigOpt = this.mergeAllConfigOpt(configOpt, harConfigOpts);

    // 用项目配置覆盖模块的bundleName和api版本信息
    this.mergeDslConfig(mergedConfigOpt);

    // debug模式添加debug: true
    if (this.targetService.isDebug()) {
      const deviceConfigObj: ConfigJson.DeviceConfigObj = mergedConfigOpt.deviceConfig;
      if (deviceConfigObj.default === undefined) {
        mergedConfigOpt.deviceConfig.default = {};
      }
      mergedConfigOpt.deviceConfig.default.debug = true;
    }

    if (this.supportLiteDeviceModule(mergedConfigOpt) && !mergedConfigOpt.module.package) {
      mergedConfigOpt.module.package = ShellUtils.JS_SHELL_PACKAGE;
    }

    // 将merged json写入文件
    fs.outputJSONSync(this._mergedModuleJson, mergedConfigOpt, { spaces: '\t' });
  }

  private mergeAsanAndTsan(configOpt: ConfigOptObj): void {
    if (this.asanEnable) {
      configOpt.app.asanEnabled = this.asanEnable;
    }
    if (this.tsanEnable) {
      configOpt.app.tsanEnabled = this.tsanEnable;
    }
    if (this.hwAsanEnable) {
      configOpt.app.hwasanEnabled = this.hwAsanEnable;
    }
    if (this.ubsanEnable) {
      configOpt.app.ubsanEnabled = this.ubsanEnable;
    }
  }

  private mergeAppEnvironmentConfig(configOpt: ConfigOptObj): void {
    const appEnvironmentObjs = this.parseToAppEnvironments(this.appEnvironments);
    if (configOpt.app.appEnvironments !== undefined && configOpt.app.appEnvironments.length !== 0) {
      this._log.debug('Change app appEnvironment');
      const envMap = new Map<string, string>();
      appEnvironmentObjs.forEach((env) => {
        envMap.set(env.name, env.value);
      });
      configOpt.app.appEnvironments?.forEach((env) => {
        envMap.set(env.name, env.value);
      });
      configOpt.app.appEnvironments = Array.from(envMap, ([name, value]) => ({ name, value }));
    } else {
      this._log.debug('Use cli appEnvironment');
      configOpt.app.appEnvironments = appEnvironmentObjs;
    }
  }

  supportLiteDeviceModule(mergedConfigOpt: ConfigOptObj): boolean {
    const deviceTypes = mergedConfigOpt.module.deviceType;
    return deviceTypes && (deviceTypes.includes(DeviceTypeConst.LITE_WEARABLE) || deviceTypes.includes(DeviceTypeConst.SMART_VISION));
  }

  private mergeAllConfigOpt(mainConfig: ConfigOptObj, harConfigOpts: ConfigOptObj[]): ConfigOptObj {
    for (let index = 0; index < harConfigOpts.length; index++) {
      const harModuleOpt = harConfigOpts[index];
      if (harModuleOpt !== null) {
        // api版本兼容，hap的compatible要大于等于三方包的compatible
        if (
          harModuleOpt.app.apiVersion?.compatible &&
          mainConfig.app.apiVersion?.compatible &&
          harModuleOpt.app.apiVersion?.compatible > mainConfig.app.apiVersion?.compatible
        ) {
          this._log.printErrorExit(
              'COMPATIBLE_VERSION_TOO_LOW',
              [harModuleOpt.module.name || harModuleOpt.module.distro?.moduleName || ''],
            [[harModuleOpt.app.apiVersion.compatible]]
            );
        }
        mainConfig = <ConfigOptObj>this.mergeModel(mainConfig, harModuleOpt, 'config', index === harConfigOpts.length - 1);
      }
    }
    return mainConfig;
  }

  /**
   * 合并对象。策略：
   * 1. hap不为空，har为空：合并结果为hap的值
   * 2. hap为空，har不为空：合并结果为har的值
   * 3. hap与har的值相同：合并结果为hap的值
   * 4. hap与har的值不同：需要mergeRule
   *
   * @param mainModel 主模型对象
   * @param subModel 副模型对象
   * @param {string} clazz 当前合并对象的标签字段
   * @param needCleanMergeRule 是否需要清除MergeRule
   * @returns {any} 合并后的模型
   * @private
   */
  private mergeModel(mainModel: Option, subModel: Option, clazz: string, needCleanMergeRule: boolean): Option {
    if (LegacyMergeProfile.enumInclude(KeepHapOptions, clazz) || subModel === undefined) {
      return mainModel;
    }
    if (mainModel === undefined) {
      return subModel;
    }
    const keys: string[] = union(Object.keys(mainModel), Object.keys(subModel));
    keys.forEach((key) => {
      const mainValue = mainModel[key];
      const subValue = subModel[key];
      if (mainValue && subValue && typeof mainValue !== typeof subValue) {
        this._log.printErrorExit(
          'TYPE_OF_VALUE_IN_CONFIG_IS_NOT_SAME',
          [key, this.moduleName]
        );
      }
      this.checkFieldRule(mainModel, subModel, key, clazz);
      const curType: string = mainValue === undefined ? typeof subValue : typeof mainValue;
      switch (curType) {
        case 'boolean':
          LegacyMergeProfile.handleBooleanMerge(mainModel, mainValue, subValue, key);
          break;

        /*
         * 布尔类型的字段，除了har存在而hap不存在的场景，用hap覆盖har配置
         * 字符串和整数类型的字段，如果是module或者deviceConfig字段中的内容，即便hap没有配置，也使用hapValue；
         * 否则har存在而hap不存在的场景，用hap覆盖har配置
         */
        case 'string':
        case 'number':
          LegacyMergeProfile.handleStringOrNumberMerge(mainModel, mainValue, subValue, key, clazz);
          break;

        // 因为js的类型系统，typeof无法判断是数组还是对象或map类型的字段，需要另加判断
        case 'object':
          if (Array.isArray(mainValue) || Array.isArray(subValue)) {
            // 数组类型
            this.handleArrayMerge(mainModel, mainValue as unknown[], subValue as unknown[], key, clazz, needCleanMergeRule);
          } else if (LegacyMergeProfile.enumInclude(IsMapFields, key)) {
            /*
             * map类型。json直接转化成js对象后，无法识别一个对象是我们规范的配置文件字段，
             * 还是一个字段所对应的map，此处用枚举类型判断
             */
            this.handleMapMerge(mainModel, mainValue as Option, subValue as Option, key, clazz, needCleanMergeRule);
          } else {
            mainModel[key] = this.mergeModel(mainValue as Option, subValue as Option, key, needCleanMergeRule);
          }
          break;
        default:
          this._log.error('An unrecognized type!');
          break;
      }
    });
    return mainModel;
  }

  /**
   * 对合并时对象为数组时的处理。
   * 对string[]，可以直接取并集；
   * 对object[]，同样是取并集，但以name字段作为并集的依据，需要合并name字段冲突的对象
   *
   * @param {any[]} mainValue
   * @param {any[]} subValue
   * @param {string} field
   * @param needCleanMergeRule
   * @returns {any[]}
   * @private
   */
  private mergeList<T extends { mergeRule?: string[] }>(mainValue: T[], subValue: T[], field: string, needCleanMergeRule: boolean): T[] {
    let mergedList: T[] = [];

    if (field === 'skills') {
      return mainValue;
    }
    const curType: string = mainValue.length === 0 ? typeof subValue[0] : typeof mainValue[0];
    switch (curType) {
      case 'string':
        // string[]，取数组并集
        mergedList = union(mainValue, subValue);
        break;
      case 'object':
        // 有占位符需要替换的字段，先替换占位符
        if (LegacyMergeProfile.enumInclude(HasPlaceholderOptions, field)) {
          mainValue.forEach((value) => this.disposePlaceHolder(value, field));
          subValue.forEach((value) => this.disposePlaceHolder(value, field));
        }

        /*
         * 手动操作取并集：
         * 1. 先加入mainValue中的不冲突的mainElement
         */
        mainValue.forEach((mainElement) => {
          const subElement = this.findObjectByUniqueKey(subValue, mainElement, field);
          if (subElement === null) {
            mergedList.push(mainElement);
          }
        });
        subValue.forEach((subElement) => {
          const mainElement = this.findObjectByUniqueKey(mainValue, subElement, field);
          if (mainElement === null) {
            // 2. 加入subValue中不冲突的subElement
            mergedList.push(subElement);
          } else {
            // 3. 处理冲突，合并对象后加入数组
            const mergeElement = this.mergeModel(mainElement, subElement, field, needCleanMergeRule);
            mergedList.push(<T>this.mergeRule(mergeElement, mainElement));
          }
        });
        break;
      default:
        this._log.error(`Other Type! ${field}`);
    }
    if (needCleanMergeRule) {
      for (const value of mergedList) {
        if (Object.keys(value).includes('mergeRule')) {
          value['mergeRule'] = undefined;
        }
      }
    }
    return mergedList;
  }

  /**
   * 在判断冲突对象时，需要由一个UniqueKey作为识别位判断是否冲突。这个函数是用来在一个list中找到与model冲突的object的。
   * @param {Option[]} list 目标数组
   * @param {Option} model 提供uniqueKey的对象
   * @param {string} field 当前所在的option字段
   * @returns {Option | null} 若找到冲突的object，返回它，否则返回null
   * @private
   */
  private findObjectByUniqueKey(list: Option[], model: Option, field: string): Option | null {
    // 获取uniqueKey
    const uniqueKey = LegacyMergeProfile.getUniqueKey(model, field);
    for (const subModel of list) {
      // 在list中遍历每个model，获取uniqueKey并比较
      const compareKey = LegacyMergeProfile.getUniqueKey(subModel, field);

      // abilities中，如果targetAbility和ability.name冲突，报编译错误
      if (field === 'abilities') {
        const mainTargetAbilityName = model['targetAbility'];
        const subTargetAbilityName = subModel['targetAbility'];
        if (!mainTargetAbilityName && subTargetAbilityName && mainTargetAbilityName === uniqueKey) {
          this._log.printErrorExit(
            'TARGET_ABILITY_NAME_CONFLICT',
            [mainTargetAbilityName, this.moduleName]
          );
        }
      }
      if (uniqueKey === compareKey) {
        return subModel;
      }
    }
    return null;
  }

  /**
   * 获取当前model的uniqueKey
   * @param {Option} model 当前model
   * @param {string} clazz 当前model所属的字段
   * @returns {string | null} 如果有uniqueKey，返回它，否则返回null
   * @private
   */
  private static getUniqueKey(model: Option, clazz: string): string | null {
    if (LegacyMergeProfile.enumInclude(UniqueKeyEqualsName, clazz)) {
      return model['name'] as string;
    } else if (clazz === 'shortcuts') {
      return model['shortcutId'] as string;
    } else if (clazz === 'abilities') {
      if (model['targetAbility'] === undefined) {
        return model['name'] as string;
      } else {
        return model['targetAbility'] as string;
      }
    } else {
      return null;
    }
  }

  /**
   * 对冲突的model，需要根据mergeRule来进行合并
   * - remove标记合并后，需要移除的属性
   * - replace标记合并冲突时需要替换的标签，始终保留高优先级的值
   *
   * @param {Option} mergeValue 主合并的model
   * @param {Option} subValue 副model
   * @returns {Option} 合并后的model
   * @private
   */
  private mergeRule(mergeValue: Option, subValue: Option): Option {
    if (subValue === null || subValue.mergeRule === undefined) {
      return mergeValue;
    }
    const mergeRule = subValue.mergeRule as MergeRuleObj;
    if (mergeRule.replace) {
      const replaceLabel: string[] = mergeRule.replace;
      replaceLabel.forEach((fieldItem) => {
        if (mergeValue[fieldItem]) {
          mergeValue[fieldItem] = subValue[fieldItem];
        }
      });
    }
    if (mergeRule.remove) {
      const removeLabel: string[] = mergeRule.remove;
      removeLabel.forEach((fieldItem) => {
        if (mergeValue[fieldItem]) {
          mergeValue[fieldItem] = undefined;
        }
      });
    }
    return mergeValue;
  }

  /**
   * 合并实际上应该是map类型的对象
   *
   * @param {Option} mainValue
   * @param {Option} subValue
   * @param {boolean} flag
   * @param {string} key
   * @param needCleanMergeRule
   * @returns {Option}
   * @private
   */
  private mergeMap(mainValue: Option, subValue: Option, flag: boolean, key: string, needCleanMergeRule: boolean) {
    if (mainValue === undefined) {
      return mainValue;
    }
    if (subValue === undefined) {
      return subValue;
    }

    // 取并集
    const mergedMap = cloneDeep(mainValue);
    const mainKeys = Object.keys(mainValue);
    const subKeys = Object.keys(subValue);
    const addKeys = difference(subKeys, mainKeys);

    if (flag) {
      addKeys.forEach((addKey) => {
        mergedMap[addKey] = subValue[addKey];
      });
    }

    const sameKeys = intersection(subKeys, mainKeys);

    // 对重复冲突的键值对，采取跟mergeArray类似的合并方法
    sameKeys.forEach((sameKey) => {
      if (Array.isArray(mainValue[sameKey])) {
        mergedMap[sameKey] = union(mainValue[sameKey] as unknown[], subValue[sameKey] as unknown[]);
      } else if (typeof mainValue[sameKey] === 'object') {
        mergedMap[sameKey] = this.mergeModel(mainValue[sameKey] as Option, subValue[sameKey] as Option, key, needCleanMergeRule);
      }
    });
    return mergedMap;
  }

  /**
   * 判断当前model是否有配相对应的mergeRule来合并，如果mergeRule配置有误则报错
   *
   * @param {Option} mainModel 主model
   * @param {Option} subModel 副model
   * @param {string} key 需要检查的键
   * @param {string} clazz 当前的字段
   * @private
   */
  private checkFieldRule(mainModel: Option, subModel: Option, key: string, clazz: string): void {
    // 判断当前字段是否需要mergeRule来决定合并合法性
    if (!LegacyMergeProfile.enumInclude(IsNeedMergeRule, clazz) || LegacyMergeProfile.enumInclude(KeepAbilityMergeList, clazz) || key === 'skills') {
      return;
    }
    const mainValue = mainModel[key];
    const subValue = subModel[key];
    if (mainValue === undefined || subValue === undefined) {
      return;
    }

    // 值不为空且不同，需要mergeRule
    if (!isEqual(mainValue, subValue)) {
      const mergeRule = mainModel['mergeRule'] as MergeRuleObj;
      const mergeRuleList = union(mergeRule.replace, mergeRule.remove);
      if (!mergeRuleList.includes(key)) {
        const uniqueKey = LegacyMergeProfile.getUniqueKey(mainValue as Option, clazz);
        this._log.printErrorExit(
          'MERGE_RULE_CONFLICT',
          [clazz, uniqueKey, key, this.moduleName]
        );
      }
    }
  }

  /**
   * 处理占位符
   *
   * @param {Option} config 当前model
   * @param {string} clazz 当前字段
   * @returns {Option}
   * @private
   */
  private disposePlaceHolder(config: Option, clazz: string): Option {
    const keys: string[] = Object.keys(config);
    keys.forEach((key) => {
      if (LegacyMergeProfile.enumInclude(NeedDisposePlaceholder, key) && !(clazz === 'abilities' && key === 'name')) {
        const keyObj = config[key];
        if (typeof keyObj === 'string') {
          config[key] = keyObj.replace('{bundleName}', this._appBundleName!);
        } else if (Array.isArray(keyObj) && keyObj.length !== 0) {
          if (typeof keyObj[0] === 'string') {
            const newArray: string[] = [];
            for (const value of config[key] as string[]) {
              newArray.push(value.replace('{bundleName}', this._appBundleName!));
            }
            config[key] = newArray;
          } else {
            const newArray: Named[] = [];
            for (const value of config[key] as Named[]) {
              value.name = value.name?.replace('{bundleName}', this._appBundleName!);
              newArray.push(value);
            }
            config[key] = newArray;
          }
        }
      }
    });
    return config;
  }

  static enumInclude<T>(enumObj: T, value: string) {
    // @ts-ignore
    return Object.values(enumObj).includes(value as unknown as T);
  }

  private static handleBooleanMerge(mainModel: Option, mainValue: unknown, subValue: unknown, key: string) {
    if (LegacyMergeProfile.enumInclude(KeepOneOfThem, key)) {
      mainModel[key] = mainValue === undefined && subValue !== undefined ? subValue : mainValue;
    }
  }

  private static handleStringOrNumberMerge(mainModel: Option, mainValue: unknown, subValue: unknown, key: string, clazz: string) {
    let realValue = mainValue === undefined && subValue !== undefined ? subValue : mainValue;
    if (clazz === 'deviceConfig' || clazz === 'module') {
      realValue = mainValue;
    }
    mainModel[key] = realValue;
  }

  private handleArrayMerge(mainModel: Option, mainValue: unknown[], subValue: unknown[], key: string, clazz: string, needCleanMergeRule: boolean) {
    const mainList = Array.isArray(mainValue) ? mainValue : [];
    const subList = Array.isArray(subValue) ? subValue : [];
    let curFieldList: unknown[];
    if (clazz === 'module' && LegacyMergeProfile.enumInclude(KeepModuleAttr, key)) {
      curFieldList = [...mainList];
    } else {
      curFieldList = this.mergeList(mainList, subList, key, needCleanMergeRule);
    }
    if (curFieldList.length !== 0) {
      mainModel[key] = curFieldList;
    }
  }

  private handleMapMerge(mainModel: Option, mainValue: Option, subValue: Option, key: string, clazz: string, needCleanMergeRule: boolean) {
    let flag = true;
    if (clazz === 'config') {
      flag = false;
      if (mainValue['default'] === undefined && subValue['default'] !== undefined) {
        if ((subValue['default'] as DeviceConfigOptionObj)['allowComponentsProxy'] === undefined) {
          subValue['default'] = undefined;
        } else {
          mainValue['default'] = {};
        }
      }
    }
    mainModel[key] = this.mergeMap(mainValue, subValue, flag, key, needCleanMergeRule);
  }

  declareInputFiles(): FileSet {
    const inputFiles = new FileSet();
    inputFiles.addEntry(this._moduleTargetRes.getJsonPath());
    this._harLibs.forEach((harConfigJson) => {
      if (fs.existsSync(harConfigJson)) {
        inputFiles.addEntry(harConfigJson);
      }
    });
    return inputFiles;
  }

  declareOutputFiles(): FileSet {
    return new FileSet().addEntry(this._mergedModuleJson);
  }
}
